/**
 *
 * The MIT License
 *
 * Copyright (c) 2008 Board of Trustees, Leland Stanford Jr. University. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:

 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.macdadi.ks.client.views;

import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.*;
import org.macdadi.core.client.auth.AuthService;
import org.macdadi.core.client.auth.AuthenticatedUser;
import org.macdadi.core.client.auth.AuthorizationException;
import org.macdadi.core.client.domain.*;
import org.macdadi.core.client.ui.DefaultItemEditor;
import org.macdadi.core.client.ui.EditableTree;
import org.macdadi.ks.client.View;
import org.macdadi.ks.client.ViewContainer;

import java.util.ArrayList;

/**
 * @author Ben Suter <ben.suter@gmail.com>
 */
public class Stakeholders extends View implements TreeListener
{
    protected Grid treeGrid;
    protected EditableTree stakeholdersTree, designersTree, decisionMakersTree, projectMembersTree;
    protected ArrayList stakeholdersExpandedItems = new ArrayList();
    protected ArrayList designersExpandedItems = new ArrayList();
    protected ArrayList projectMembersExpandedItems = new ArrayList();
    protected ArrayList decisionMakersExpandedItems = new ArrayList();

    protected ProjectInfo project;

    protected EditableTree.ItemEditor groupAdd, groupRemove, groupEdit, memberAdd, memberRemove;
    protected EditableTree.ItemEditor designerAdd, designerRemove;
    protected EditableTree.ItemEditor projectMemberAdd, projectMemberRemove;
    protected EditableTree.ItemEditor decisionMakerAdd, decisionMakerRemove;

    public static ViewInfo init()
    {
        String info = "<p>View and manage stakeholders, designers, decision makers and project membership</p>";

        return new ViewInfo("Stakeholders", info)
        {
            public View createInstance(ViewContainer container)
            {
                return new Stakeholders(container);
            }

            public String getColor()
            {
                return "#bf2a2a";
            }
        };
    }

    public Stakeholders(ViewContainer container)
    {
        this.container = container;
        project = container.getCurrentProject();

        HorizontalPanel hp = new HorizontalPanel();
        hp.setStyleName("macdadi-Stakeholders");
        hp.setWidth("100%");

        treeGrid = new Grid(2, 2);
        hp.add(treeGrid);

        stakeholdersTree = new EditableTree();
        treeGrid.setWidget(1, 0, stakeholdersTree);
        stakeholdersTree.addTreeListener(this);

        designersTree = new EditableTree();
        treeGrid.setWidget(0, 1, designersTree);
        designersTree.addTreeListener(this);

        decisionMakersTree = new EditableTree();
        treeGrid.setWidget(0, 0, decisionMakersTree);
        decisionMakersTree.addTreeListener(this);

        projectMembersTree = new EditableTree();
        treeGrid.setWidget(1, 1, projectMembersTree);
        projectMembersTree.addTreeListener(this);

        groupAdd = new GroupAdd(expandSelectedThenUpdateStakeholdersTree, project);
        groupEdit = new GroupEdit(updateStakeholdersTree, project);
        groupRemove = new GroupRemove(updateStakeholdersTree, project);
        memberAdd = new MemberAdd(expandSelectedThenUpdateStakeholdersTree, project);
        memberRemove = new MemberRemove(updateStakeholdersTree, project);

        designerAdd = new DesignerAdd(expandSelectedThenUpdateDesignersTree, project);
        designerRemove = new DesignerRemove(updateDesignersTree, project);

        projectMemberAdd = new ProjectMemberAdd(expandSelectedThenUpdateProjectMembersTree, project);
        projectMemberRemove = new ProjectMemberRemove(updateProjectMembersTree, project);

        decisionMakerAdd = new DecisionMakerAdd(expandSelectedThenUpdateDecisionMakersTree, project);
        decisionMakerRemove = new DecisionMakerRemove(updateDecisionMakersTree, project);

        initWidget(hp);
    }

    public void onShow()
    {
        populateStakeholdersTree(project);
        populateDesignersTree(project);
        populateProjectMembersTree(project);
        populateDecisionMakersTree(project);

        AuthenticatedUser user = container.getCurrentUser();

        if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_CREATE)) {
            stakeholdersTree.addItemEditor(groupAdd);
            stakeholdersTree.addItemEditor(memberAdd);
            designersTree.addItemEditor(designerAdd);
            projectMembersTree.addItemEditor(projectMemberAdd);
            decisionMakersTree.addItemEditor(decisionMakerAdd);
        }

        if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_REMOVE)) {
            stakeholdersTree.addItemEditor(groupRemove);
            stakeholdersTree.addItemEditor(memberRemove);
            designersTree.addItemEditor(designerRemove);
            projectMembersTree.addItemEditor(projectMemberRemove);
            decisionMakersTree.addItemEditor(decisionMakerRemove);
        }

        if (user.isAuthorizedOperation(project.getId(), AuthService.TARGET_STAKEHOLDERS, AuthService.OPERATION_UPDATE))
            stakeholdersTree.addItemEditor(groupEdit);

    }

    public void onHide()
    {
        stakeholdersTree.stopEditing();
        stakeholdersTree.removeItemEditor(groupAdd);
        stakeholdersTree.removeItemEditor(groupRemove);
        stakeholdersTree.removeItemEditor(groupEdit);
        stakeholdersTree.removeItemEditor(memberAdd);
        stakeholdersTree.removeItemEditor(memberRemove);
        stakeholdersTree.clear();

        designersTree.stopEditing();
        designersTree.removeItemEditor(designerAdd);
        designersTree.removeItemEditor(designerRemove);
        designersTree.clear();

        projectMembersTree.stopEditing();
        projectMembersTree.removeItemEditor(projectMemberAdd);
        projectMembersTree.removeItemEditor(projectMemberRemove);
        projectMembersTree.clear();

        decisionMakersTree.stopEditing();
        decisionMakersTree.removeItemEditor(decisionMakerAdd);
        decisionMakersTree.removeItemEditor(decisionMakerRemove);
        decisionMakersTree.clear();
    }

    /**
     * Reload the current user object, to refresh the user's
     * privilege meta-data. This method should be called
     * whenever a privilege may have been added
     * or removed from the currently authenticated user.
     *
     * The purpose of this method is simply to allow the UI to update
     * itself for the current user as a convenience: for example,
     * if the decision-maker makes themself a stakeholder, they should
     * be able to start adding goals immediately, without first logging
     * out and then in again.
     *
     * We invoke this on: stakeholder add/remove, designer add/remove,
     * and decision-maker remove.
     */
    protected void reloadCurrentUser()
    {
        container.getAuthenticationControl().reloadAuthenticatedUser();
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level groups for this project and adds each one to the
     * tree. As it does so, the children (Users) of these groups are added.
     * <p/>
     * Finally the rootItem is expanded, so that the first level of groups
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateStakeholdersTree(final ProjectInfo project)
    {
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading stakeholders for " + project.getName());
        stakeholdersTree.addItem(rootItem);

        DataService.App.getInstance().getStakeholderGroups(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
        {
            public void onFailure(Throwable caught)
            {
                GWT.log("Failed to populate the stakeholders tree", caught);

                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view stakeholders for " + project.getName());
                }
            }

            public void onSuccess(Object result)
            {
                rootItem.setText("Stakeholders");

                StakeholderGroup[] roots = (StakeholderGroup[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addStakeholdersChild(rootItem, roots[i]);
                }

                /* Expand the first level */
                rootItem.setState(true, false);
            }
        });
    }

    /**
     * Adds the given StakeholderGroup as a child TreeItem to the parent TreeItem.
     * Steps into the given group's members and adds each of those too.
     *
     * @param parent the tree node to act as parent for the newly added StakeholderGroup
     * @param child  the StakeholderGroup to be added as a child node to the tree
     */
    protected void addStakeholdersChild(TreeItem parent, StakeholderGroup child)
    {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        for (int i = 0; i < child.getNumMembers(); i++)
            addStakeholdersChild(node, child.getMember(i));

        /* If this node, as identified by its group's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (stakeholdersExpandedItems.contains(computeIdFromShg(child)))
            node.setState(true, false);
    }

    /**
     * Adds the given User as a child TreeItem to the parent TreeItem. It is
     * expected that the parent TreeItem represents a StakeholderGroup.
     *
     * @param parent the StakeholderGroup TreeItem
     * @param child  the member (User)
     */
    protected void addStakeholdersChild(TreeItem parent, User child)
    {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        /* If this node, as identified by its stakeholder's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (stakeholdersExpandedItems.contains(computeIdFromUser(child)))
            node.setState(true, false);
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level groups for this project and adds each one to the
     * tree. As it does so, the children (Users) of these groups are added.
     * <p/>
     * Finally the rootItem is expanded, so that the first level of groups
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateDesignersTree(final ProjectInfo project)
    {
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading designers for " + project.getName());
        designersTree.addItem(rootItem);

        DataService.App.getInstance().getDesigners(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
        {
            public void onFailure(Throwable caught)
            {
                GWT.log("Failed to populate the designers tree", caught);

                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view designers for " + project.getName());
                }
            }

            public void onSuccess(Object result)
            {
                rootItem.setText("Designers");

                User[] roots = (User[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addDesignersChild(rootItem, roots[i]);
                }

                /* Expand the first level */
                rootItem.setState(true, false);
            }
        });
    }

    /**
     * Adds the given User as a child TreeItem to the parent TreeItem. It is
     * expected that the parent TreeItem represents a StakeholderGroup.
     *
     * @param parent the StakeholderGroup TreeItem
     * @param child  the member (User)
     */
    protected void addDesignersChild(TreeItem parent, User child)
    {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        /* If this node, as identified by its stakeholder's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (designersExpandedItems.contains(computeIdFromUser(child)))
            node.setState(true, false);
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level groups for this project and adds each one to the
     * tree. As it does so, the children (Users) of these groups are added.
     * <p/>
     * Finally the rootItem is expanded, so that the first level of groups
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateProjectMembersTree(final ProjectInfo project)
    {
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading project members for " + project.getName());
        projectMembersTree.addItem(rootItem);

        DataService.App.getInstance().getProjectMembers(project.getId(), true, -1, container.getCurrentAuthToken(), new AsyncCallback()
        {
            public void onFailure(Throwable caught)
            {
                GWT.log("Failed to populate the project members tree", caught);

                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view project members for " + project.getName());
                }
            }

            public void onSuccess(Object result)
            {
                rootItem.setText("Project members");

                User[] roots = (User[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addProjectMembersChild(rootItem, roots[i]);
                }

                /* Expand the first level */
                rootItem.setState(true, false);
            }
        });
    }

    /**
     * Adds the given User as a child TreeItem to the parent TreeItem. It is
     * expected that the parent TreeItem represents a StakeholderGroup.
     *
     * @param parent the StakeholderGroup TreeItem
     * @param child  the member (User)
     */
    protected void addProjectMembersChild(TreeItem parent, User child)
    {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        /* If this node, as identified by its stakeholder's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (projectMembersExpandedItems.contains(computeIdFromUser(child)))
            node.setState(true, false);
    }

    /**
     * Add a new root item to the tree, whose label initially displays a "loading ..." message.
     * Then it retrieves all the root-level groups for this project and adds each one to the
     * tree. As it does so, the children (Users) of these groups are added.
     * <p/>
     * Finally the rootItem is expanded, so that the first level of groups
     * is always visible.
     *
     * @param project the project from which the tree domain should be pulled
     */
    protected void populateDecisionMakersTree(final ProjectInfo project)
    {
        final TreeItem rootItem = new TreeItem();
        rootItem.setText("Loading decision makers for " + project.getName());
        decisionMakersTree.addItem(rootItem);

        DataService.App.getInstance().getDecisionMakers(project.getId(), container.getCurrentAuthToken(), new AsyncCallback()
        {
            public void onFailure(Throwable caught)
            {
                GWT.log("Failed to populate the decision makers tree", caught);

                if (caught instanceof AuthorizationException) {
                    rootItem.setText("Please login to view decision makers for " + project.getName());
                }
            }

            public void onSuccess(Object result)
            {
                rootItem.setText("Decision makers");

                User[] roots = (User[]) result;
                for (int i = 0; i < roots.length; i++) {
                    addDecisionMakersChild(rootItem, roots[i]);
                }

                /* Expand the first level */
                rootItem.setState(true, false);
            }
        });
    }

    /**
     * Adds the given User as a child TreeItem to the parent TreeItem. It is
     * expected that the parent TreeItem represents a StakeholderGroup.
     *
     * @param parent the StakeholderGroup TreeItem
     * @param child  the member (User)
     */
    protected void addDecisionMakersChild(TreeItem parent, User child)
    {
        TreeItem node = new TreeItem(child.getName());
        node.setUserObject(child);
        parent.addItem(node);

        /* If this node, as identified by its stakeholder's id, was "expanded" in a
         * previous incarnation of this tree, then expand it here again.
         */
        if (decisionMakersExpandedItems.contains(computeIdFromUser(child)))
            node.setState(true, false);
    }

    public void onTreeItemSelected(TreeItem item)
    {

    }

    /**
     * Remember the expansion state of this item, so that we can later
     * return the tree to the same state when it is refreshed.
     *
     * @param item the item whose expanded/folded state changed
     */
    public void onTreeItemStateChanged(TreeItem item)
    {
        /* Remember the expansion state for later tree refresh, based on ids and object types. */
        String id;
        id = computeIdFromUserObject(item.getUserObject());
        if (id == null)
            return;

        if (item.getTree().equals(stakeholdersTree)) {
            if (item.getState())
                stakeholdersExpandedItems.add(id);
            else
                stakeholdersExpandedItems.remove(id);
        } else if (item.getTree().equals(designersTree)) {
            if (item.getState())
                designersExpandedItems.add(id);
            else
                designersExpandedItems.remove(id);
        } else if (item.getTree().equals(projectMembersTree)) {
            if (item.getState())
                projectMembersExpandedItems.add(id);
            else
                projectMembersExpandedItems.remove(id);
        } else if (item.getTree().equals(decisionMakersTree)) {
            if (item.getState())
                decisionMakersExpandedItems.add(id);
            else
                decisionMakersExpandedItems.remove(id);
        }
    }

    // TODO: practically we can ignore users, because they (for now) don't
    // contain children so are never expanded !
    protected String computeIdFromUserObject(Object userObject)
    {
        if (userObject instanceof User)
            return computeIdFromUser((User) userObject);
        else if (userObject instanceof StakeholderGroup)
            return computeIdFromShg((StakeholderGroup) userObject);
        else
            return null;
    }

    protected String computeIdFromUser(User u)
    {
        return "user-" + Integer.toString(u.getId());
    }

    protected String computeIdFromShg(StakeholderGroup shg)
    {
        return "shg-" + Integer.toString(shg.getId());
    }


    /**
     * A command that refreshes the tree.
     */
    protected Command updateStakeholdersTree = new Command()
    {
        public void execute()
        {
            stakeholdersTree.clear();
            populateStakeholdersTree(project);
        }
    };

    /**
     * A command that refreshes the tree.
     */
    protected Command updateDesignersTree = new Command()
    {
        public void execute()
        {
            designersTree.clear();
            populateDesignersTree(project);
        }
    };

    /**
     * A command that refreshes the tree.
     */
    protected Command updateProjectMembersTree = new Command()
    {
        public void execute()
        {
            projectMembersTree.clear();
            populateProjectMembersTree(project);
        }
    };

    /**
     * A command that refreshes the tree.
     */
    protected Command updateDecisionMakersTree = new Command()
    {
        public void execute()
        {
            decisionMakersTree.clear();
            populateDecisionMakersTree(project);
        }
    };

    /**
     * A command that first expands the selected tree item, and then
     * refreshes the tree. This is typically used when a new child
     * gets added to a node, so that the new child will be visible
     * after the refresh.
     */
    protected Command expandSelectedThenUpdateStakeholdersTree = new Command()
    {
        public void execute()
        {
            /* If the item being edited is not already expanded, "remember"
             * it as expanded for after the refresh.
             */
            TreeItem edited = stakeholdersTree.getSelectedItem();
            if (edited != null) {
                String id;
                id = computeIdFromUserObject(edited.getUserObject());
                if (id != null && !stakeholdersExpandedItems.contains(id))
                    stakeholdersExpandedItems.add(id);
            }

            updateStakeholdersTree.execute();
        }
    };

    /**
     * A command that first expands the selected tree item, and then
     * refreshes the tree. This is typically used when a new child
     * gets added to a node, so that the new child will be visible
     * after the refresh.
     */
    protected Command expandSelectedThenUpdateDesignersTree = new Command()
    {
        public void execute()
        {
            /* If the item being edited is not already expanded, "remember"
             * it as expanded for after the refresh.
             */
            TreeItem edited = designersTree.getSelectedItem();
            if (edited != null) {
                String id;
                id = computeIdFromUserObject(edited.getUserObject());
                if (id != null && !designersExpandedItems.contains(id))
                    designersExpandedItems.add(id);
            }

            updateDesignersTree.execute();
        }
    };

    /**
     * A command that first expands the selected tree item, and then
     * refreshes the tree. This is typically used when a new child
     * gets added to a node, so that the new child will be visible
     * after the refresh.
     */
    protected Command expandSelectedThenUpdateProjectMembersTree = new Command()
    {
        public void execute()
        {
            /* If the item being edited is not already expanded, "remember"
             * it as expanded for after the refresh.
             */
            TreeItem edited = projectMembersTree.getSelectedItem();
            if (edited != null) {
                String id;
                id = computeIdFromUserObject(edited.getUserObject());
                if (id != null && !projectMembersExpandedItems.contains(id))
                    projectMembersExpandedItems.add(id);
            }

            updateProjectMembersTree.execute();
        }
    };

    /**
     * A command that first expands the selected tree item, and then
     * refreshes the tree. This is typically used when a new child
     * gets added to a node, so that the new child will be visible
     * after the refresh.
     */
    protected Command expandSelectedThenUpdateDecisionMakersTree = new Command()
    {
        public void execute()
        {
            /* If the item being edited is not already expanded, "remember"
             * it as expanded for after the refresh.
             */
            TreeItem edited = decisionMakersTree.getSelectedItem();
            if (edited != null) {
                String id;
                id = computeIdFromUserObject(edited.getUserObject());
                if (id != null && !decisionMakersExpandedItems.contains(id))
                    decisionMakersExpandedItems.add(id);
            }

            updateDecisionMakersTree.execute();
        }
    };

    /*
    * Editing:
    *
    * root:
    *   Add StakeholderGroup
    *
    * StakeholderGroup:
    *   Edit name - need groupId
    *   Remove group (and members from group) - need groupId
    *   Add member (only if contains no sub-groups) - need groupId, memberId
    *    choose from project members
    *    add new project member, from list of users
    *    create user and add to project
    *   (FUTURE) Add sub-group (only if contains no members) - need groupId, newName
    *   (FUTURE) Assign color
    *
    * Member:
    *   Remove from group - need groupId and userId
    *
    */

    /**
     * Lets you add a new group at the root of the tree,
     * providing an input field for the name of the new group.
     * <p/>
     * This editor only matches the root TreeItem.
     * <p/>
     * In the future, when nested groups are possible, this editor
     * will match to group nodes as well as the root.
     */
    public class GroupAdd extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public GroupAdd(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathAdd, "Add a Stakeholder Group", onSuccess);
            project = p;
        }

        public boolean matchesItem(TreeItem item)
        {
            return item.getParentItem() == null;
        }

        protected Widget getInputWidget(Object userObject)
        {
            // note: as long as this only matches to the root, the userObject will always be null
            return configureWidget((StakeholderGroup) userObject);
        }

        protected Widget configureWidget(final StakeholderGroup group)
        {
            /* A panel for editing the name: text input, apply, cancel */
            VerticalPanel vpA = new VerticalPanel();
            final TextBox tbAdd = new TextBox();
            tbAdd.setVisibleLength(50);
            vpA.add(tbAdd);
            HorizontalPanel hpA = new HorizontalPanel();
            Button btnAddDo = new Button("Create");
            btnAddDo.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    addChildGroup(group, tbAdd.getText().trim());
                }
            });
            hpA.add(btnAddDo);

            Button btnAddCancel = new Button("Reset");
            btnAddCancel.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    tbAdd.setText("");
                }
            });
            hpA.add(btnAddCancel);
            vpA.add(hpA);

            return vpA;
        }

        protected void addChildGroup(final StakeholderGroup group, String newName)
        {
            if (newName.length() == 0) {
                displayError("You must enter a name for the new group", null);
                return;
            }

            int parent = -1;
            if (group != null)
                parent = group.getId();

            DataWriteService.App.getInstance().createStakeholderGroup(project.getId(), parent, newName, container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Only matches to TreeItems whose userObject is a StakeholderGroup.
     */
    public abstract class StakeholderGroupEditor extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public StakeholderGroupEditor(String iconPath, String title, Command onSuccess, ProjectInfo p)
        {
            super(iconPath, title, onSuccess);
            this.project = p;
        }

        /* Only display this for StakeholderGroup nodes */
        public boolean matchesItem(TreeItem item)
        {
            return (item.getUserObject() instanceof StakeholderGroup);
        }

        protected Widget getInputWidget(Object userObject)
        {
            if (userObject == null || !(userObject instanceof StakeholderGroup))
                throw new IllegalArgumentException("userObject must be a valid StakeholderGroup");

            return configureWidget((StakeholderGroup) userObject);
        }

        protected abstract Widget configureWidget(final StakeholderGroup group);
    }

    /**
     * Lets you edit the StakeholderGroup's name.
     */
    public class GroupEdit extends StakeholderGroupEditor
    {
        public GroupEdit(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathEdit, "Edit", onSuccess, p);
        }

        protected Widget configureWidget(final StakeholderGroup group)
        {
            /* A panel for editing the name: text input, apply, cancel */
            VerticalPanel vpE = new VerticalPanel();
            final TextBox tbEdit = new TextBox();
            tbEdit.setVisibleLength(50);
            tbEdit.setText(group.getName());
            vpE.add(tbEdit);

            HorizontalPanel hpE = new HorizontalPanel();

            Button btnEditDo = new Button("Apply");
            btnEditDo.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    String newName = tbEdit.getText().trim();
                    updateGroup(group, newName);
                }
            });
            hpE.add(btnEditDo);

            Button btnEditCancel = new Button("Reset");
            btnEditCancel.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    tbEdit.setText(group.getName());
                }
            });
            hpE.add(btnEditCancel);
            vpE.add(hpE);

            return vpE;
        }

        protected void updateGroup(final StakeholderGroup group, String newName)
        {
            if (newName.equals(group.getName()))
                return; /* No change was made */

            DataWriteService.App.getInstance().updateStakeholderGroup(project.getId(), group.getId(), newName, container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Displays a clickable link to remove the group.
     */
    public class GroupRemove extends StakeholderGroupEditor
    {
        public GroupRemove(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathRemove, "Remove", onSuccess, p);
        }

        protected Widget configureWidget(final StakeholderGroup group)
        {
            HTML rem = new HTML("<u style='cursor:hand'>Remove this group</u>");
            rem.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    removeGroup(group);
                }
            });

            return rem;
        }

        protected void removeGroup(StakeholderGroup group)
        {
            DataWriteService.App.getInstance().removeStakeholderGroup(project.getId(), group.getId(), container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Lets you add a member to the StakeholderGroup.
     */
    public class MemberAdd extends StakeholderGroupEditor
    {
        public MemberAdd(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathAdd, "Add a member", onSuccess, p);
        }

        protected Widget configureWidget(final StakeholderGroup group)
        {
            /* A panel for adding a new member */
            VerticalPanel vp = new VerticalPanel();

            final ListBox lb = new ListBox();
            lb.addItem("Choose a project member", "-1");
            fillAvailableProjectMembers(lb, group);
            vp.add(lb);

            Button btnAdd = new Button("Add to " + group.getName());
            btnAdd.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    int newMember = -1;

                    if (lb.getSelectedIndex() > 0) {
                        try {
                            newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
                        } catch (Exception e) {
                            newMember = -1;
                        }
                    }

                    if (newMember > 0) {
                        addMember(group, newMember);
                    }
                }
            });
            vp.add(btnAdd);

            return vp;
        }

        protected void addMember(final StakeholderGroup group, int userId)
        {
            DataWriteService.App.getInstance().addMemberToStakeholderGroup(project.getId(), group.getId(), userId, container.getCurrentAuthToken(), this);

            /* If the current user is modifying their own privileges, reload the user object to reflect this. */
            if ( userId == container.getCurrentUser().getId() )
                reloadCurrentUser();
        }

        protected void fillAvailableProjectMembers(final ListBox choices, StakeholderGroup thisGroup)
        {
            DataService.App.getInstance().getProjectMembers(project.getId(), true, thisGroup.getId(), container.getCurrentAuthToken(), new AsyncCallback()
            {
                public void onFailure(Throwable caught)
                {
                    displayError("Unable to retrieve the available project members: " + caught.getMessage(), caught);
                    if (caught instanceof AuthorizationException) {
                        displayError("Please login to view available project members", caught);
                    }
                }

                /**
                 * If the result is null this is interpreted as failure.
                 *
                 * @param result the result of the async call, should be an array of users
                 */
                public void onSuccess(Object result)
                {
                    if (result == null || !(result instanceof User[])) {
                        displayError("Unable to retrieve the available project members", null);
                        return;
                    }

                    User[] members = (User[]) result;
                    for (int i = 0; i < members.length; i++) {
                        choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
                    }
                }
            });
        }
    }

    /**
     * Only matches to TreeItems whose userObject is a User.
     */
    public class MemberRemove extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public MemberRemove(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathRemove, "Remove from group", onSuccess);
            this.project = p;
        }

        /* Only display this for User (member) nodes */
        public boolean matchesItem(TreeItem item)
        {
            /* In the future, may need to check that parent is a group too */
            return (item.getUserObject() instanceof User);
        }

        protected Widget getInputWidget(Object userObject)
        {
            if (userObject == null || !(userObject instanceof User))
                throw new IllegalArgumentException("userObject must be a valid User");

            return configureWidget((User) userObject);
        }


        protected Widget configureWidget(final User member)
        {
            // TODO: add checks to make this safe, or at least make matchesItem tighter
            TreeItem parent = item.getParentItem();
            final StakeholderGroup group = (StakeholderGroup) parent.getUserObject();

            HTML rem = new HTML("<u style='cursor:hand'>Remove this member from the group</u>");
            rem.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    removeMember(group, member);
                }
            });

            return rem;
        }

        protected void removeMember(StakeholderGroup group, User member)
        {
            DataWriteService.App.getInstance().removeMemberFromStakeholderGroup(project.getId(), group.getId(), member.getId(), container.getCurrentAuthToken(), this);

            /* If the current user is modifying their own privileges, reload the user object to reflect this. */
            if ( member.getId() == container.getCurrentUser().getId() )
                reloadCurrentUser();
        }

    }

    /**
     * Only matches to TreeItems whose userObject is a User.
     */
    public class DesignerRemove extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public DesignerRemove(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathRemove, "Remove designer", onSuccess);
            this.project = p;
        }

        /* Only display this for User (member) nodes */
        public boolean matchesItem(TreeItem item)
        {
            return (item.getUserObject() instanceof User);
        }

        protected Widget getInputWidget(Object userObject)
        {
            if (userObject == null || !(userObject instanceof User))
                throw new IllegalArgumentException("userObject must be a valid User");

            return configureWidget((User) userObject);
        }

        protected Widget configureWidget(final User user)
        {
            // TODO: add checks to make this safe, or at least make matchesItem tighter

            HTML rem = new HTML("<u style='cursor:hand'>Remove this designer</u>");
            rem.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    removeDesigner(user);
                }
            });

            return rem;
        }

        protected void removeDesigner(User user)
        {
            DataWriteService.App.getInstance().removeDesigner(project.getId(), user.getId(), container.getCurrentAuthToken(), this);

            /* If the current user is modifying their own privileges, reload the user object to reflect this. */
            if ( user.getId() == container.getCurrentUser().getId() )
                reloadCurrentUser();
        }
    }

    /**
     * Only matches to TreeItems whose userObject is a User.
     */
    public class ProjectMemberRemove extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public ProjectMemberRemove(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathRemove, "Remove project member", onSuccess);
            this.project = p;
        }

        /* Only display this for User (member) nodes */
        public boolean matchesItem(TreeItem item)
        {
            return (item.getUserObject() instanceof User);
        }

        protected Widget getInputWidget(Object userObject)
        {
            if (userObject == null || !(userObject instanceof User))
                throw new IllegalArgumentException("userObject must be a valid User");

            return configureWidget((User) userObject);
        }

        protected Widget configureWidget(final User user)
        {
            // TODO: add checks to make this safe, or at least make matchesItem tighter

            HTML rem = new HTML("<u style='cursor:hand'>Remove this project member</u>");
            rem.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    removeProjectMember(user);
                }
            });

            return rem;
        }

        protected void removeProjectMember(User user)
        {
            DataWriteService.App.getInstance().removeProjectMember(project.getId(), user.getId(), container.getCurrentAuthToken(), this);
        }
    }

    /**
     * Only matches to TreeItems whose userObject is a User.
     */
    public class DecisionMakerRemove extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public DecisionMakerRemove(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathRemove, "Remove decision maker", onSuccess);
            this.project = p;
        }

        /* Only display this for User (member) nodes */
        public boolean matchesItem(TreeItem item)
        {
            return (item.getUserObject() instanceof User);
        }

        protected Widget getInputWidget(Object userObject)
        {
            if (userObject == null || !(userObject instanceof User))
                throw new IllegalArgumentException("userObject must be a valid User");

            return configureWidget((User) userObject);
        }

        protected Widget configureWidget(final User user)
        {
            // TODO: add checks to make this safe, or at least make matchesItem tighter

            HTML rem = new HTML("<u style='cursor:hand'>Remove this decision maker</u>");
            rem.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    removeDecisionMaker(user);
                }
            });

            return rem;
        }

        protected void removeDecisionMaker(User user)
        {
            DataWriteService.App.getInstance().removeDecisionMaker(project.getId(), user.getId(), container.getCurrentAuthToken(), this);

            /* If the current user is modifying their own privileges, reload the user object to reflect this. */
            if ( user.getId() == container.getCurrentUser().getId() )
                reloadCurrentUser();  
        }
    }

    /**
     * Lets you add a new designer at the root of the tree,
     * providing a list of current project members.
     * <p/>
     * This editor only matches the root TreeItem.
     */
    public class DesignerAdd extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public DesignerAdd(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathAdd, "Add a designer", onSuccess);
            project = p;
        }

        public boolean matchesItem(TreeItem item)
        {
            return item.getParentItem() == null;
        }

        protected Widget getInputWidget(Object userObject)
        {
            // note: as long as this only matches to the root, the userObject will always be null
            return configureWidget();
        }

        protected Widget configureWidget()
        {
            /* A panel for adding a new member */
            VerticalPanel vp = new VerticalPanel();

            final ListBox lb = new ListBox();
            lb.addItem("Choose a project member", "-1");
            fillAvailableProjectMembers(lb);
            vp.add(lb);

            Button btnAdd = new Button("Add");
            btnAdd.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    int newMember = -1;

                    if (lb.getSelectedIndex() > 0) {
                        try {
                            newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
                        } catch (Exception e) {
                            newMember = -1;
                        }
                    }

                    if (newMember > 0) {
                        addDesigner(newMember);
                    }
                }
            });
            vp.add(btnAdd);

            return vp;
        }

        protected void addDesigner(int userId)
        {
            DataWriteService.App.getInstance().addDesigner(project.getId(), userId, container.getCurrentAuthToken(), this);

            /* If the current user is modifying their own privileges, reload the user object to reflect this. */
            if ( userId == container.getCurrentUser().getId() )
                reloadCurrentUser();
        }

        protected void fillAvailableProjectMembers(final ListBox choices)
        {
            DataService.App.getInstance().getProjectMembers(project.getId(), true, -1, container.getCurrentAuthToken(), new AsyncCallback()
            {
                public void onFailure(Throwable caught)
                {
                    displayError("Unable to retrieve the available project members: " + caught.getMessage(), caught);
                    if (caught instanceof AuthorizationException) {
                        displayError("Please login to view available project members", caught);
                    }
                }

                /**
                 * If the result is null this is interpreted as failure.
                 *
                 * @param result the result of the async call, should be an array of users
                 */
                public void onSuccess(Object result)
                {
                    if (result == null || !(result instanceof User[])) {
                        displayError("Unable to retrieve the available project members", null);
                        return;
                    }

                    User[] members = (User[]) result;
                    for (int i = 0; i < members.length; i++) {
                        choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
                    }
                }
            });
        }


    }

    /**
     * Lets you add a new decision maker at the root of the tree,
     * providing a list of current project members.
     * <p/>
     * This editor only matches the root TreeItem.
     */
    public class DecisionMakerAdd extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public DecisionMakerAdd(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathAdd, "Add a decision maker", onSuccess);
            project = p;
        }

        public boolean matchesItem(TreeItem item)
        {
            return item.getParentItem() == null;
        }

        protected Widget getInputWidget(Object userObject)
        {
            // note: as long as this only matches to the root, the userObject will always be null
            return configureWidget();
        }

        protected Widget configureWidget()
        {
            /* A panel for adding a new member */
            VerticalPanel vp = new VerticalPanel();

            final ListBox lb = new ListBox();
            lb.addItem("Choose a project member", "-1");
            fillAvailableProjectMembers(lb);
            vp.add(lb);

            Button btnAdd = new Button("Add");
            btnAdd.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    int newMember = -1;

                    if (lb.getSelectedIndex() > 0) {
                        try {
                            newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
                        } catch (Exception e) {
                            newMember = -1;
                        }
                    }

                    if (newMember > 0) {
                        addDecisionMaker(newMember);
                    }
                }
            });
            vp.add(btnAdd);

            return vp;
        }

        protected void addDecisionMaker(int userId)
        {
            DataWriteService.App.getInstance().addDecisionMaker(project.getId(), userId, container.getCurrentAuthToken(), this);
        }

        protected void fillAvailableProjectMembers(final ListBox choices)
        {
            DataService.App.getInstance().getProjectMembers(project.getId(), true, -1, container.getCurrentAuthToken(), new AsyncCallback()
            {
                public void onFailure(Throwable caught)
                {
                    displayError("Unable to retrieve the available project members: " + caught.getMessage(), caught);
                    if (caught instanceof AuthorizationException) {
                        displayError("Please login to view available project members", caught);
                    }
                }

                /**
                 * If the result is null this is interpreted as failure.
                 *
                 * @param result the result of the async call, should be an array of users
                 */
                public void onSuccess(Object result)
                {
                    if (result == null || !(result instanceof User[])) {
                        displayError("Unable to retrieve the available project members", null);
                        return;
                    }

                    User[] members = (User[]) result;
                    for (int i = 0; i < members.length; i++) {
                        choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
                    }
                }
            });
        }


    }

    /**
     * Lets you add a new designer at the root of the tree,
     * providing a list of current project members.
     * <p/>
     * This editor only matches the root TreeItem.
     */
    public class ProjectMemberAdd extends DefaultItemEditor
    {
        protected ProjectInfo project;

        public ProjectMemberAdd(Command onSuccess, ProjectInfo p)
        {
            super(DefaultItemEditor.iconPathAdd, "Add a project member", onSuccess);
            project = p;
        }

        public boolean matchesItem(TreeItem item)
        {
            return item.getParentItem() == null;
        }

        protected Widget getInputWidget(Object userObject)
        {
            // note: as long as this only matches to the root, the userObject will always be null
            return configureWidget();
        }

        protected Widget configureWidget()
        {
            /* A panel for adding a new member */
            VerticalPanel vp = new VerticalPanel();

            final ListBox lb = new ListBox();
            lb.addItem("Choose a user", "-1");
            fillAvailableUsers(lb);
            vp.add(lb);

            Button btnAdd = new Button("Add");
            btnAdd.addClickListener(new ClickListener()
            {
                public void onClick(Widget sender)
                {
                    int newMember = -1;

                    if (lb.getSelectedIndex() > 0) {
                        try {
                            newMember = Integer.parseInt(lb.getValue(lb.getSelectedIndex()));
                        } catch (Exception e) {
                            newMember = -1;
                        }
                    }

                    if (newMember > 0) {
                        addProjectMember(newMember);
                    }
                }
            });
            vp.add(btnAdd);

            return vp;
        }

        protected void addProjectMember(int userId)
        {
            DataWriteService.App.getInstance().addProjectMember(project.getId(), userId, container.getCurrentAuthToken(), this);
        }

        protected void fillAvailableUsers(final ListBox choices)
        {
            DataService.App.getInstance().findUsers(project.getId(), true, true, null, container.getCurrentAuthToken(), new AsyncCallback()
            {
                public void onFailure(Throwable caught)
                {
                    displayError("Unable to retrieve the available users: " + caught.getMessage(), caught);
                    if (caught instanceof AuthorizationException) {
                        displayError("Please login to view available users", caught);
                    }
                }

                /**
                 * If the result is null this is interpreted as failure.
                 *
                 * @param result the result of the async call, should be an array of users
                 */
                public void onSuccess(Object result)
                {
                    if (result == null || !(result instanceof User[])) {
                        displayError("Unable to retrieve the available users", null);
                        return;
                    }

                    User[] members = (User[]) result;
                    for (int i = 0; i < members.length; i++ ) {
                          choices.addItem(members[i].getName(), Integer.toString(members[i].getId()));
                      }
                  }
              });
          }



    }
}
